/*
Copyright 2010,2011 Matt Van Der Westhuizen

This file is part of CGLL.

CGLL is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

CGLL is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public License
along with CGLL.  If not, see <http://www.gnu.org/licenses/>.
*/

package org.chaoticengine.cgll.collision;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.chaoticengine.cgll.entity.Entity;
import org.chaoticengine.cgll.entity.component.ActiveRenderComponent;
import org.chaoticengine.cgll.input.Command;
import org.newdawn.slick.Color;
import org.newdawn.slick.GameContainer;
import org.newdawn.slick.Graphics;
import org.newdawn.slick.geom.Rectangle;
import org.newdawn.slick.geom.Shape;
import org.newdawn.slick.state.StateBasedGame;

/**
 * This broad phase collision detector partitions the screen into squares of
 * equal size and uses the squares as buckets, placing each entity into all
 * buckets it intersects with. The narrow phase then simply compares all
 * entities in each bucket, which in theory should provide a big saving on the
 * number of comparisons that need to be performed.
 *
 * @author Matt v.d. Westhuizen
 */
public class PartitionBroadPhaseCollisionDetector implements IBroadPhaseCollisionDetector{
    private INarrowPhaseCollisionDetector npCd = null;
    private int scrWidth = 0;
    private int scrHeight = 0;
    private int bucketSize = 0;
    private int bucketRows = 0;
    private int bucketCols = 0;
    private List<List<Entity>> buckets = null;
    private List<Entity> all = null;
    
    public PartitionBroadPhaseCollisionDetector(INarrowPhaseCollisionDetector npCd,
            int screenWidthPx, int screenHeightPx, int bucketSizePx) {
        this.npCd = npCd;
        this.scrWidth = screenWidthPx;
        this.scrHeight = screenHeightPx;
        this.bucketSize = bucketSizePx;
        this.bucketCols = scrWidth / bucketSize;
        this.bucketRows = scrHeight / bucketSize;
        this.all = new ArrayList<Entity>();

        // Create the buckets
        int numBuckets = (scrWidth / bucketSize) * (scrHeight / bucketSize);
        this.buckets = new ArrayList<List<Entity>>(numBuckets);

        for (int i = 0; i < numBuckets; i++) {
            this.buckets.add(new ArrayList<Entity>());
        }
    }

    public void addEntities(List<Entity> entities) {
        for (int i = 0; i < entities.size(); i++) {
            addEntity(entities.get(i));
        }
    }

    public void addEntity(Entity entity) {
        all.add(entity);

        Shape bounds = entity.getBounds();

        int col = (int) (bounds.getCenterX() - bounds.getBoundingCircleRadius()) / bucketSize;
        int row = (int) (bounds.getCenterY() - bounds.getBoundingCircleRadius()) / bucketSize;
        int maxCol = (int) (bounds.getCenterX() + bounds.getBoundingCircleRadius()) / bucketSize;
        int maxRow = (int) (bounds.getCenterY() + bounds.getBoundingCircleRadius()) / bucketSize;

        for (int r = row; r <= maxRow; r++) {
            for (int c = col; c <= maxCol; c++) {
                List<Entity> bucket = getBucket(c, r);

                if (bucket != null) {
                    bucket.add(entity);
                }
            }
        }
    }

    public void removeEntity(Entity entity) {
        all.remove(entity);

        Shape bounds = entity.getBounds();

        int col = (int) (bounds.getCenterX() - bounds.getBoundingCircleRadius()) / bucketSize;
        int row = (int) (bounds.getCenterY() - bounds.getBoundingCircleRadius()) / bucketSize;
        int maxCol = (int) (bounds.getCenterX() + bounds.getBoundingCircleRadius()) / bucketSize;
        int maxRow = (int) (bounds.getCenterY() + bounds.getBoundingCircleRadius()) / bucketSize;

        for (int r = row; r <= maxRow; r++) {
            for (int c = col; c <= maxCol; c++) {
                List<Entity> bucket = getBucket(c, r);

                if (bucket != null) {
                    bucket.remove(entity);
                }
            }
        }
    }

    public void doHitDetection() {
        // Only do hit detection and basket updates on moving entities
        for (int i = 0; i < all.size(); i++) {
            Entity e = all.get(i);

            if (e.isMoving()) {
                // Check for bucket changes
                Shape bounds = e.getBounds();

                int col = (int) (bounds.getCenterX() - bounds.getBoundingCircleRadius()) / bucketSize;
                int row = (int) (bounds.getCenterY() - bounds.getBoundingCircleRadius()) / bucketSize;
                int maxCol = (int) (bounds.getCenterX() + bounds.getBoundingCircleRadius()) / bucketSize;
                int maxRow = (int) (bounds.getCenterY() + bounds.getBoundingCircleRadius()) / bucketSize;

                for (int r = row - 1; r <= maxRow + 1; r++) {
                    for (int c = col - 1; c <= maxCol + 1; c++) {
                        List<Entity> bucket = getBucket(c, r);

                        if (bucket != null) {
                            if ((r < row) || (c < col) || (r > maxRow) || (c > maxCol)) {
                                bucket.remove(e);
                            } else {
                                if (!bucket.contains(e)) {
                                    bucket.add(e);
                                }

                                // Check for collisions in relevant buckets
                                bucketCollisionCheck(bucket, e);
                            }
                        }
                    }
                }
            }
        }
    }

    public INarrowPhaseCollisionDetector getNarrowPhaseHitDetector() {
        return(this.npCd);
    }

    public void setNarrowPhaseHitDetector(INarrowPhaseCollisionDetector npHd) {
        this.npCd = npHd;
    }

    /**
     * Returns the bucket for the specified position, or null if it is outside
     * the screen (hits outside the screen are ignored).
     * 
     * @param col The column.
     * @param row The row.
     * @return The bucket at the defined position.
     */
    private List<Entity> getBucket(int col, int row) {
        int index = row * bucketCols + col;

        if ((index < 0) || (index >= buckets.size())) {
            return(null);
        } else {
            return(buckets.get(index));
        }
    }

    private void bucketCollisionCheck(List<Entity> b, Entity e) {
        for (int i = 0; i < b.size(); i++) {
            Entity f = b.get(i);

            if (e != f) {
                if (npCd.isHit(e, f)) {
                    e.getComponentManager().handleCollision(f);
                    f.getComponentManager().handleCollision(e);
                }
            }
        }
    }

    public DebugRenderComponent getDebugRenderComponent() {
        return(new DebugRenderComponent());
    }

    public class DebugRenderComponent extends ActiveRenderComponent {
        private Map<String, Command> commands = new HashMap<String, Command>();

        public DebugRenderComponent() {
            super();
        }

        @Override
        public void render(GameContainer gc, StateBasedGame sb, Graphics gr) {
            Color color = gr.getColor();
            gr.setColor(Color.white);

            for (int row = 0; row < bucketRows; row++) {
                for (int col = 0; col < bucketCols; col++) {
                    Rectangle rect = new Rectangle(
                            col * bucketSize,
                            row * bucketSize,
                            bucketSize,
                            bucketSize);
                    gr.draw(rect);
                    gr.drawString(getBucket(col, row).size() + "",
                            col * bucketSize, row * bucketSize);
                }
            }

            gr.setColor(color);
        }

        @Override
        public Map<String, Command> getCommands() {
            return(commands);
        }


    }
}
